package com.mattcofer.cartoonwidget;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.Queue;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import com.mattcofer.cartoonwidget.CartoonProvider.Cartoons;
import com.mattcofer.cartoonwidget.CartoonProvider.CartoonsColumns;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.IBinder;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.util.Log;
import android.widget.RemoteViews;

/**
 * Background service to build any requested widget updates. Uses a single
 * background thread to walk through an update queue, querying
 * {@link WebserviceHelper} as needed to fill database. Also could handle
 * scheduling of future updates, like 3AM EST since Dilbert is updated around
 * 2AM CST.
 */
public class UpdateService extends Service implements Runnable {
	private static final String TAG = "CartoonWidget";
	
	private static Context _context;
    
    private static final String[] PROJECTION_APPWIDGET = {
    	CartoonsColumns.URL
    };	
	
    /**
     * Lock used when maintaining queue of requested updates.
     */
	private static Object sLock = new Object();

    /**
     * Internal queue of requested widget updates. You <b>must</b> access
     * through {@link #requestUpdate(int[])} or {@link #getNextUpdate()} to make
     * sure your access is correctly synchronized.
     */
	private static Queue<Integer> sAppWidgetIds = new LinkedList<Integer>();
    
    /**
     * Flag if there is an update thread already running. We only launch a new
     * thread if one isn't already running.
     */
	private static boolean sThreadRunning = false;
    
    /**
     * Specific {@link Intent#setAction(String)} used when performing a full
     * update of all widgets, usually when an update alarm goes off.
     */
	public static final String ACTION_UPDATE_ALL = "com.mattcofer.dilbertwidget.UPDATE_ALL";

    /**
     * Peek if we have more updates to perform. This method is special because
     * it assumes you're calling from the update thread, and that you will
     * terminate if no updates remain. (It automatically resets
     * {@link #sThreadRunning} when none remain to prevent race conditions.)
     */
    private static boolean hasMoreUpdates() {
        synchronized (sLock) {
            boolean hasMore = !sAppWidgetIds.isEmpty();
            
            if (!hasMore) {
                sThreadRunning = false;
                Log.d(TAG, ".UpdateService$hasMoreUpdates: No updates remain");
            }
            else
            {
            	Log.d(TAG, ".UpdateService$hasMoreUpdates: " + sAppWidgetIds.size() + " update(s) remain");
            }
            return hasMore;
        }
    }

    /**
     * Poll the next widget update in the queue.
     */
    private static int getNextUpdate() {
        synchronized (sLock) {
            if (sAppWidgetIds.peek() == null) {
                return AppWidgetManager.INVALID_APPWIDGET_ID;
            } else {
                return sAppWidgetIds.poll();
            }
        }
    }
 
	@Override
	public IBinder onBind(Intent intent) {
		// TODO Auto-generated method stub
		return null;
	}

	 /**
     * Start this service, creating a background processing thread, if not
     * already running. If started with {@link #ACTION_UPDATE_ALL}, will
     * automatically add all widgets to the requested update queue.
     */
    @Override
    public void onStart(Intent intent, int startId) {
    	super.onStart(intent, startId);
    	Log.d(TAG, ".UpdateService$onStart: Started widget update service");
    	
    	/*
    	 * This is not only called when there is real work to do, it is called
    	 * instantly upon adding a new widget to the desktop
    	 * 
    	 */
    	
        // If requested, trigger update of all widgets
        if (ACTION_UPDATE_ALL.equals(intent.getAction())) {
            AppWidgetManager manager = AppWidgetManager.getInstance(this);
            int[] appWidgetIds = manager.getAppWidgetIds(new ComponentName(this, CartoonWidget.class));
            String idsString = "";
            for (int i=0; i<appWidgetIds.length; i++)
            {
            	idsString += (i==0 ? "" : ", ") + appWidgetIds[i]; 
            }
            Log.d(TAG, ".UpdateService$onStart Updating all widgets; IDs: ["+idsString+"]");
            requestUpdate(appWidgetIds);
        }
        else
        {
        	Log.d(TAG, ".UpdateService$onStart Action: " + intent.getAction());	
        }

        // Only start processing thread if not already running
        synchronized (sLock)
        {
            if (!sThreadRunning)
            {
                sThreadRunning = true;
                new Thread(this).start();
            }
        }

       	Log.d(TAG, ".UpdateService$onStart Action is null so we are doing nothing...");	
   	
    }	
    
    /**
     * Request updates for the given widgets. Will only queue them up, you are
     * still responsible for starting a processing thread if needed, usually by
     * starting the parent service.
     */
    public static void requestUpdate(int[] appWidgetIds) {
        synchronized (sLock) {
            for (int appWidgetId : appWidgetIds) {
                sAppWidgetIds.add(appWidgetId);
            }
        }
    }
	
    /**
    * Main thread for running through any requested widget updates until none
    * remain. Also sets alarm to perform next update.
    */    
	public void run() {
		Log.d(TAG, ".UpdateService$run() thread started");
		AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);

		Cartoon cartoon = null;
		
        while (hasMoreUpdates()) 
        {
            int appWidgetId = getNextUpdate();
            Uri appWidgetUri = ContentUris.withAppendedId(Cartoons.CONTENT_URI, appWidgetId);

            try {
				cartoon = updateWidget(this, appWidgetUri);
			} catch (Exception e) {
				Log.e(TAG, "The following exception occurred while attempting to update widget " + appWidgetId + ":", e);
			}

        	RemoteViews updateViews = null;
        	updateViews = CartoonWidget.buildUpdate(this, appWidgetUri);
      
        	//If we have an update, push it to the widget
        	if (updateViews!=null)
        	{
        		Log.d(TAG, "Updating widget ID: " + appWidgetId);
        		appWidgetManager.updateAppWidget(appWidgetId, updateViews);
        	}
        }

        scheduleNextUpdate(DateUtils.MINUTE_IN_MILLIS);

        //No updates remain and we're done with this thread, so stop this service
        stopSelf();
	}
	
	public void scheduleNextUpdate(long timeUntilUpdate)
	{
        //Setup the time for one hour in the future
        Time time = new Time();
        time.set(System.currentTimeMillis() + DateUtils.MINUTE_IN_MILLIS);
        time.second = 0;
        
        //Calculate the time delta purely for logging purposes
        long nextUpdate = time.toMillis(false);
        long nowMillis = System.currentTimeMillis();
        long deltaSeconds = (nextUpdate - nowMillis) / DateUtils.SECOND_IN_MILLIS;
        Log.d(TAG, "Scheduling next update at " + nextUpdate + ", which is in " + deltaSeconds + " seconds.");

        //Build intent that will be fired
        Intent updateIntent = new Intent(ACTION_UPDATE_ALL);
        updateIntent.setClass(this, UpdateService.class);
        PendingIntent pendingIntent = PendingIntent.getService(this, 0, updateIntent, 0);

        //Schedule alarm, and force the device awake for this update
        AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
        alarmManager.set(AlarmManager.RTC_WAKEUP, nextUpdate, pendingIntent);
	}

    private static class Cartoon {
    	Bitmap cartoon;
    }

    /*
     * Updates the widget identified by the provided uri
     */
    public static Cartoon updateWidget(Context context, Uri appWidgetUri) throws Exception {
    	Log.d(TAG, ".WebserviceHelper$updateWidgets Update initiated for widget (Uri: " + appWidgetUri.toString() + ")");
    	
    	_context = context;
    	
        Uri appWidgetCartoons = Uri.withAppendedPath(appWidgetUri, Cartoons.TWIG_CARTOONS);
        
        Log.d(TAG, ".WebserviceHelper$updateWidgets appWidgetCartoons=" +appWidgetCartoons.toString());
        
        ContentResolver resolver = context.getContentResolver();

        Cursor cursor = null;
        String cartoonUrl = "";

        //Get the cartoon's URL from the database, identified by the widget's Uri
        try {
            cursor = resolver.query(appWidgetUri, PROJECTION_APPWIDGET, null, null, null);
            if (cursor != null && cursor.moveToFirst()) {
            	cartoonUrl = cursor.getString(cursor.getColumnIndex(CartoonsColumns.URL));
            	Log.d(TAG, ".WebserviceHelper$updateDilberts Retrieved url:" + cartoonUrl);
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        //Query the RSS feed
        Cartoon cartoon = null;
		try {
			cartoon = getCartoon(cartoonUrl);
		} catch (Exception e) {
			throw new Exception("Error retrieving the cartoon: " + e.getMessage());
		}

        if (cartoon == null) {
            throw new Exception("No cartoon found at url");
        }
        
        return cartoon;
    }
    
    /**
     * Query the given rss feed and parse any returned data into a set of
     * {@link Cartoon} objects. This is a blocking call while waiting for the
     * webservice to return.
     * @throws IOException 
     * @throws ClientProtocolException 
     */
    private static Cartoon getCartoon(String cartoonUrl) throws Exception, ClientProtocolException, IOException 
    {
        if (cartoonUrl.trim().equals(""))
        {
            throw new Exception(".WebserviceHelper$queryRSSFeed Unable to query the RSS feed because the URL is blank");
        } else {
            Log.d(TAG, String.format(".WebserviceHelper$queryRSSFeed called on cartoonUrl=%s", cartoonUrl));
        }

        HttpResponse response;
        Cartoon cartoon = null;

        // Perform web service query and parse result
        HttpClient client = new DefaultHttpClient();
        HttpGet request = new HttpGet(String.format(cartoonUrl));

        response = client.execute(request);
        
        // If response found, send through to parser
        if (response != null) {
        	cartoon = parseResponse(response);
        }

        return cartoon;
    }
     
    /**
     * Returns the image from the response as a Cartoon object
     * @throws IOException 
     * @throws IllegalStateException 
     */
    private static Cartoon parseResponse(HttpResponse response) throws Exception, IllegalStateException, IOException
    {
    	Log.d(TAG, ".WebServiceHelpler$parseResponse called");
    	
    	InputStream inputStream = response.getEntity().getContent();

    	Cartoon cartoon = new Cartoon();
    	
		//---------------------------------------------------------------
		// Convert the image into a bitmap object
		//
		BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
		cartoon.cartoon = BitmapFactory.decodeStream(bufferedInputStream);
        
		return cartoon;
    }
	

}
